package com.kamistoat.meimeistore.order.web;

import com.alipay.api.AlipayApiException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.kamistoat.common.To.EsTo.OrderListTo;
import com.kamistoat.common.To.LoginSuccessTo;
import com.kamistoat.common.exception.ALLRunTimeException;
import com.kamistoat.common.runTimeFunc.RunTimeLog;
import com.kamistoat.common.utils.PageUtils;
import com.kamistoat.common.utils.R;
import com.kamistoat.meimeistore.order.To.OrderCreateTo;
import com.kamistoat.meimeistore.order.config.AlipayTemplate;
import com.kamistoat.meimeistore.order.entity.OrderEntity;
import com.kamistoat.meimeistore.order.feign.ThirdPartyFeignService;
import com.kamistoat.meimeistore.order.interceptor.LoginInterceptor;
import com.kamistoat.meimeistore.order.service.OrderService;
import com.kamistoat.meimeistore.order.vo.*;
import com.kamistoat.meimeistore.order.vo.OrderCheckVo;
import com.kamistoat.meimeistore.order.vo.OrderSubmitVo;
import com.kamistoat.meimeistore.order.vo.PayVo;
import com.kamistoat.meimeistore.order.vo.SubmitOrderResponseVo;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Controller
public class IndexController {

    @Autowired
    OrderService orderService;
    @Autowired
    AlipayTemplate alipayTemplate;
    @Autowired
    RedissonClient redissonClient;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    ThirdPartyFeignService thirdPartyFeignService;


    /**
     * 以下四个方法都是简单映射，均已弃用，没有使用到
     */
    @GetMapping("/waitpay.html")
    public String waitPayPage() {
        return "waitpay";
    }

    @GetMapping("/ordercheck.html")
    public String orderCheckPage() {
        return "ordercheck";
    }

    @GetMapping("/pay.html")
    public String payPage() {
        return "pay";
    }


    /**
     * 在购物车页面点击结算后，调用该响应
     * 服务层查询订单价差页面需要的所有信息，保存到前端后在页面上进行显示。
     */
    @GetMapping("/toTrade")
    public String toTrade(Model model, HttpServletRequest httpServletRequest) {
        LoginSuccessTo loginSuccessTo = LoginInterceptor.loginUserThreadLocal.get();
        /**
         * 获取redisson限流器，每次访问都延长其过期时间为1小时后
         */
        RRateLimiter rateLimiter = redissonClient.getRateLimiter("order:orderCheck:rateLimit:" + loginSuccessTo.getId());
        stringRedisTemplate.expire("order:orderCheck:rateLimit:" + loginSuccessTo.getId(), 5, TimeUnit.MINUTES);
        // 限流器初始化设置为每1秒提供2个令牌。如果限流器已存在，则该命令失效
        rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
        // 获取限流器令牌，超时时间是2秒。因此只要当前只有一个账号实例，必然能够获取到
        boolean tryAcquire = rateLimiter.tryAcquire(1, 2, TimeUnit.SECONDS);

        // 没有被限流才放行之后的操作
        if (tryAcquire) {
            OrderCheckVo orderCheckVo = orderService.checkOrder();
            // 如果orderItemVoList为空，也就是打钩项已经支付了或者取消了，但是有人故意使用网址直接访问订单确认页。
            // 那就别往后进行了，直接返回到购物车。
            if (orderCheckVo.getOrderItemVoList() == null || orderCheckVo.getOrderItemVoList().size() == 0) {
                return "skiptocart";
            }
            model.addAttribute("orderCheckVo", orderCheckVo);
            model.addAttribute("orderCheckMsg", httpServletRequest.getSession().getAttribute("orderCheckMsg"));
            httpServletRequest.getSession().removeAttribute("orderCheckMsg");
            // 跳转到订单确认页
            return "ordercheck";
        } else {
            return "highfrequency";
        }
    }

    /**
     * 如果一个商品正在参与秒杀，则当点击抢购时，仅仅验证信号量锁是否放行。
     * 通过后调用此URL，跳转到订单确认页，此时已不是直面全部流量了，则可以对秒杀数据进行全面校验。
     * 专门给秒杀商品用的，和上面的普通toTrade区分
     *
     * @return 订单确认页
     */
    @GetMapping("/seckillToTrade")
    public String seckillToTrade(@RequestParam Long sessionProId,
                                 @RequestParam String randomCode,
                                 @RequestParam Long skuId,
                                 @RequestParam Integer num,
                                 Model model) {
        // 获取构建订单确认页所需要的数据
        R orderCheckVo_R = orderService.checkSeckillOrder(sessionProId, randomCode, skuId, num);
        if((int)orderCheckVo_R.get("code") == 0){
            model.addAttribute("orderCheckVo", orderCheckVo_R.get("orderCheckVo"));
            // 跳转到订单确认页
            return "ordercheck";
        }else{
            model.addAttribute("seckillCheckMsg", orderCheckVo_R.get("msg"));
            return "redirect:http://meimeistore.com/" + skuId.toString() + ".html";
        }
    }

    /**
     * 选择好收货地址确认购买商品后，点击支付
     * 跳转到该页面。此时订单已经生成并锁定库存，开始倒计时等待支付。
     * 注意这里接收的 OrderSubmitVo 只包含用户使用的积分/优惠券/实付金额等信息，并不包含到底买了那些商品
     * 因为这些会再次和数据库进行一次核对，保证用户下订单的幂等性
     *
     * @return
     */
    @PostMapping("/submitOrder")
    public String submit(OrderSubmitVo orderSubmitVo, Model model, HttpServletRequest request) {
        // 非空检查。防止有人直接用网址路由来发请求
        // 不接受秒杀订单
        if (orderSubmitVo == null || orderSubmitVo.getIsSeckill()) {
            return "redirect:http://cart.meimeistore.com/cartList.html";
        }

        // 下单，要创建订单、验证令牌、验证价格、锁定库存
        System.out.println("订单接收数据");
        String message = "下单失败";

        try {
            SubmitOrderResponseVo submitOrderResponseVo = orderService.submitOrder(orderSubmitVo);
            // 没有库存异常
            if (submitOrderResponseVo.getCode() == 0) {
                // 不再直接返回付款等待页，而是跳转到接口二次处理
                // model.addAttribute("submitResponse", submitOrderResponseVo);
                // return "pay";
                return "redirect:http://order.meimeistore.com/returnpay?orderSN="
                        + submitOrderResponseVo.getOrderEntity().getOrderSn();
            } else {
                // 失败就重试
                String redirectPath = new String();
                switch (submitOrderResponseVo.getCode()) {
                    case 1:
                        // 删除令牌机制，允许同时操作订单检查。只是不允许同时提交订单
                        // message = message + " 订单信息过期，请刷新页面后提交";
                        message = message + " 您的操作过于频繁，或存在多个登录实例，请稍后再试";
                        redirectPath = "redirect:http://order.meimeistore.com/toTrade";
                        break;
                    case 2:
                        message = message + " 您在支付期间，购物车商品发生变化，请确认后再次提交";
                        redirectPath = "redirect:http://cart.meimeistore.com/cartList.html";
                        break;
                }
                request.getSession().setAttribute("orderCheckMsg", message);
                return redirectPath;
            }
        } catch (ALLRunTimeException.NoStockException e) {
            // 有库存异常
            Long skuId = e.getSkuId();
            message = message + " " + skuId.toString() + "号商品库存不足，请减少购买数量";
            request.getSession().setAttribute("orderCheckMsg", message);
            return "redirect:http://cart.meimeistore.com/cartList.html";
        }
    }


    /**
     * 如果一个商品正在参与秒杀，当其从订单确认页提交订单时，跳转到该页面。
     * 不同于普通下订单，还要再去数据库重新验证一次库存价格等内容。
     * 秒杀订单的思路是，使用信号量锁即可解决实时库存的问题，同时价格验证是根据缓存中的秒杀信息进行验证
     *
     * @return 等待支付页面.但是注意，秒杀高并发下，只能保证缓存中一定存在订单，数据库中订单是否入库未知
     */
    @PostMapping("/seckillSubmitOrder")
    public String seckillSubmit(OrderSubmitVo orderSubmitVo, Model model, HttpServletRequest request) {
        // 非空检查。防止有人直接用网址路由来发请求
        // 不接受普通订单
        if (orderSubmitVo == null || !orderSubmitVo.getIsSeckill()) {
            return "redirect:http://cart.meimeistore.com/cartList.html";
        }

        // 提交秒杀订单
        RunTimeLog.printRunTimeLog("订单接收数据");

        R submitSeckillOrder_R = orderService.submitSeckillOrder(orderSubmitVo);
        if((int)submitSeckillOrder_R.get("code") == 0){
            OrderCreateTo orderCreateTo = (OrderCreateTo) submitSeckillOrder_R.get("order");
            return "redirect:http://order.meimeistore.com/returnpay?orderSN="
                    + orderCreateTo.getOrderEntity().getOrderSn();
        }else{
            model.addAttribute("seckillCheckMsg", submitSeckillOrder_R.get("msg"));
            return "redirect:http://meimeistore.com/" + orderSubmitVo.getSkuId().toString() + ".html";
        }
    }


    /**
     * 在等待支付的订单列表页点击返回支付，能回到支付等待页面
     * 同样使用限流器，不限制多个登录实例，但是限制冲击数据库的频率，同时锁定使用错误订单SN码故意冲击接口的行为
     *
     * P.S. 后期将秒杀整合进来。因此订单应先去redis中查是否存在
     *
     * @param orderSN 订单SN
     * @param model
     * @return
     */
    @GetMapping("/returnpay")
    public String returnPay(@RequestParam String orderSN, Model model, HttpServletRequest httpServletRequest) {
        LoginSuccessTo loginSuccessTo = LoginInterceptor.loginUserThreadLocal.get();

        /**
         * 检查账户是否已经被锁定支付功能。拦截使用错误订单SN码故意冲击接口的行为
         */
        RLock banLock = redissonClient.getLock("order:abnormalPay:banLock:" + loginSuccessTo.getId());
        if (banLock.isLocked()) {
            httpServletRequest.getSession().setAttribute("orderCheckMsg", "由于您频繁进行异常支付行为，您的账号已被锁定支付功能3分钟，超时未解锁请联系管理员处理");
            return "redirect:http://order.meimeistore.com/orderlist.html";
        } else {
            /**
             * 没有锁定，但是也不允许高强度冲击数据库
             * 获取redisson限流器，每次访问都延长其过期时间为5分钟后
             */
            RRateLimiter payWaitCheckLimiter = redissonClient.getRateLimiter("order:payWaitCheck:rateLimit:" + loginSuccessTo.getId());
            stringRedisTemplate.expire("order:payWaitCheck:rateLimit:" + loginSuccessTo.getId(), 5, TimeUnit.MINUTES);
            // 限流器初始化设置为每1秒提供2个令牌。如果限流器已存在，则该命令失效
            payWaitCheckLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
            // 获取限流器令牌，超时时间是2秒。因此只要当前只有一个账号实例，必然能够获取到
            boolean payWaitCheckAcquire = payWaitCheckLimiter.tryAcquire(1, 2, TimeUnit.SECONDS);

            if (payWaitCheckAcquire) {
                OrderEntity orderEntity = orderService.getOne(new QueryWrapper<OrderEntity>()
                        .eq("member_id", loginSuccessTo.getId())
                        .eq("order_sn", orderSN)
                        .eq("status", 0));
                // 有可能出现有人不停使用错误的orderSN直连接口。因此需要设置一个异常锁
                if (orderEntity != null) {
                    SubmitOrderResponseVo submitOrderResponseVo = new SubmitOrderResponseVo();
                    submitOrderResponseVo.setOrderEntity(orderEntity);
                    submitOrderResponseVo.setCode(0);
                    model.addAttribute("submitResponse", submitOrderResponseVo);
                    return "pay";
                } else {
                    /**
                     * 说明用户直接使用无效订单SN+外部链接访问该接口，则统计次数，如果在2分钟内出现5次异常支付，将会锁定该账号
                     */
                    RRateLimiter abnormalPayLimiter = redissonClient.getRateLimiter("order:abnormalPay:rateLimit:" + loginSuccessTo.getId());
                    stringRedisTemplate.expire("order:abnormalPay:rateLimit:" + loginSuccessTo.getId(), 5, TimeUnit.MINUTES);  // 每次延长过期时间5分钟
                    abnormalPayLimiter.trySetRate(RateType.OVERALL, 5, 2, RateIntervalUnit.MINUTES);
                    boolean abnormalPayAcquire = abnormalPayLimiter.tryAcquire();

                    // 如果已经触发账户封禁
                    if (!abnormalPayAcquire) {
                        // 设置自动过期锁，不手动解锁
                        banLock.lock(3, TimeUnit.MINUTES);
                    }

                    httpServletRequest.getSession().setAttribute("orderCheckMsg", "您提交的订单参数无效，该订单可能已被关闭。请勿持续攻击该接口，否则可能导致您的账号被封禁");
                    return "redirect:http://order.meimeistore.com/orderlist.html";
                }
            } else {
                return "highfrequency";
            }
        }
    }


    /**
     * 点击支付宝支付按钮时，进行订单检查，订单未超时且处于未支付状态则拉起支付宝支付
     * <p>
     * 注意，这里涉及到：普通订单的过期时间总是固定，但是秒杀活动的订单则不同，秒杀商品的结束时间是固定的，因此不同时间的订单的过期时间是不固定的
     * 到时候写订单系统的时候，关单系统记得要匹配一下
     * <p>
     * 幂等逻辑只限制：相同的订单不允许迸发支付。锁的持有应该从进入确切的支付手段后，直到支付信息入库。
     * 同样防止有人直接使用错误的orderSn直连接口
     *
     * @param orderSn
     * @return
     * @throws AlipayApiException
     */
    @ResponseBody
    // 因为支付宝pay方法返回html代码，因此需要用produce声明使用html文件去执行
    @GetMapping(value = "/payOrder", produces = "text/html")
    public String payOrder(@RequestParam String orderSn, HttpServletRequest httpServletRequest) throws AlipayApiException {
        RLock orderStatusModifyLock = redissonClient.getLock("order:orderStatusModify:rLock:" + orderSn);
        // 如果正在上锁，说明订单状态正在修改，则一直等，直到订单状态修改完毕
        while (orderStatusModifyLock.isLocked()) {
        }

        LoginSuccessTo loginSuccessTo = LoginInterceptor.loginUserThreadLocal.get();
        PayVo payVo = orderService.getOrderPay(orderSn);
        if (payVo == null) {
            String message = "支付失败。订单已经被关闭或已完成支付，请在 我的订单 中进行确认。请勿持续攻击该接口，否则可能导致您的账号被封禁";
            httpServletRequest.getSession().setAttribute("orderCheckMsg", message);
            // 因此订单超时的跳转也不能直接用页面名字了，也得用html代码
            return "<script language=\"javascript\" type=\"text/javascript\"> \n" +
                    "window.location.href='http://order.meimeistore.com/orderlist.html';\n" +
                    "</script>";
        } else {
            // 支付宝pay方法返回的是一段html代码，需要将其以html文件的形式执行，才能进行跳转
            String payStatus = alipayTemplate.pay(payVo);
            return payStatus;
        }
    }


    /**
     * 绕过真实支付，直接向监听接口发送支付成功回调
     *
     * @return
     */
    @GetMapping(value = "/payOrderProd")
    public String patOrderProd(@RequestParam String orderSn, HttpServletRequest httpServletRequest) {
        PayVo payVo = orderService.getOrderPay(orderSn);
        String redirectPath = "";
        if (payVo == null) {
            String message = "支付失败。订单已经被关闭，请在 我的订单 中进行确认。请勿持续攻击该接口，否则可能导致您的账号被封禁";
            httpServletRequest.getSession().setAttribute("orderCheckMsg", message);
            redirectPath = "redirect:http://order.meimeistore.com/orderlist.html";
        } else {
            // 直接绕过真实支付，向回调监听端口发送支付成功
            // 延迟两秒，模拟真实支付的用户操作
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            redirectPath = "redirect:http://order.meimeistore.com/payed/notify-prod?orderSn="
                    + payVo.getOut_trade_no() + "&payment=" + payVo.getTotal_amount();
        }
        return redirectPath;
    }

    /**
     * 微信支付响应
     *
     * @return
     */
    @GetMapping("/pauOrderWeChat")
    public String payOrderWeChat() {
        return null;
    }


    /**
     * 当前用户的所有订单详情。支付宝支付成功后同样跳转到这里。
     * 将原本的从Mysql中查询并构建，转换为从Es中进行条件查询。
     *
     * @param pageNum pageLimit 分页查询的规格设置
     * @return
     */
    @GetMapping("/orderlist.html")
    public String orderListFromES(
            @RequestParam(value = "searchCondition", defaultValue = "") String searchCondition,
            @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
            @RequestParam(value = "pageLimit", defaultValue = "5") Integer pageLimit,
            Model model,
            HttpServletRequest httpServletRequest) {
        LoginSuccessTo loginSuccessTo = LoginInterceptor.loginUserThreadLocal.get();
        // ES进行查询
        List<OrderListTo> orderListTosFromES = thirdPartyFeignService.getOrderListTosFromES(loginSuccessTo.getId(), pageNum, pageLimit, searchCondition);

        // 为了保证兼容性，还是用 IPage进行前后互联
        IPage<OrderListTo> toPage = new Page<>();
        toPage.setRecords(orderListTosFromES);
        toPage.setPages(pageNum);
        toPage.setSize(pageLimit);
        PageUtils page = new PageUtils(toPage);
        model.addAttribute("orderCheckMsg", httpServletRequest.getSession().getAttribute("orderCheckMsg"));
        httpServletRequest.getSession().removeAttribute("orderCheckMsg");
        model.addAttribute("orders", page);
        return "orderlist";
    }

    /**
     * 原本的从MySQL中查询订单并构建
     *
     * @param pageNum
     * @param pageLimit
     * @param model
     * @param httpServletRequest
     * @return
     */
    public String orderListPage(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
                                @RequestParam(value = "pageLimit", defaultValue = "5") Integer pageLimit,
                                Model model,
                                HttpServletRequest httpServletRequest) {
        // 把分页查询信息构造好
        Map<String, Object> params = new HashMap<>();
        params.put("page", pageNum.toString());
        params.put("limit", pageLimit.toString());
        PageUtils page = orderService.queryPageByUser(params);
        model.addAttribute("orderCheckMsg", httpServletRequest.getSession().getAttribute("orderCheckMsg"));
        httpServletRequest.getSession().removeAttribute("orderCheckMsg");
        model.addAttribute("orders", page);
        return "orderlist";
    }

    /**
     * 手动确认收货。
     *
     * @param orderSN
     * @param httpServletRequest
     * @return
     */
    @GetMapping("/confirmOrder")
    public String confirmOrder(@RequestParam String orderSN, HttpServletRequest httpServletRequest) {
        R confirmStatus = orderService.confirmOrder(orderSN);
        httpServletRequest.getSession().setAttribute("orderCheckMsg", confirmStatus.get("msg"));
        return "redirect:http://order.meimeistore.com/orderlist.html";
    }


    /**
     * 远程接口，被third-party 调用。当未支付的总订单入库时，Canal监听到通知这里，由这个方法构建 OrderListTo
     *
     * @param topOrderId 总订单ID
     * @param topOrderSn 总订单SN
     * @return
     */
    @ResponseBody
    @GetMapping("/getNewOrderAsync")
    public OrderListTo getNewOrderAsync(@RequestParam Long topOrderId, @RequestParam String topOrderSn) {
        OrderListTo orderListTo = orderService.getNewOrderAsync(topOrderId, topOrderSn);
        return orderListTo;
    }

    /**
     * 远程接口，被third-party 调用。当修改订单时，Canal监听到通知这里，由这个方法构建 OrderListTo
     *
     * @param topOrderId 总订单ID
     * @param topOrderSn 总订单SN
     * @return
     */
    @ResponseBody
    @GetMapping("/getUpdateOrderAsync")
    public OrderListTo getUpdateOrderAsync(@RequestParam Long topOrderId, @RequestParam String topOrderSn) {
        OrderListTo orderListTo = orderService.getUpdateOrderAsync(topOrderId, topOrderSn);
        return orderListTo;
    }


}








